Skip to content

Do not fail the sweep when a head branch is reused by a newer pull request - #9333

Merged
taladrane merged 2 commits into
mainfrom
handle-reused-head-branches
Sep 3, 2026
Merged

Do not fail the sweep when a head branch is reused by a newer pull request#9333
taladrane merged 2 commits into
mainfrom
handle-reused-head-branches

Conversation

@taladrane

Copy link
Copy Markdown
Collaborator

Follow-up to #9332. That PR fixed the sweep; this one fixes the single condition it exposed.

What #9332 achieved

The first genuine post-merge run (33774976735, running the merge commit) completed a full sweep for the first time since 19 Aug:

Before After
Branches inspected died at 57 of 1,608 1,615 — full sweep
Pull requests reconciled 0 148
Branches deleted 0 228
Runtime ~90s then abort 8m49s, ran to completion

The next run confirms steady state: 1,467 branches remaining, 1 target, 60 seconds. The backlog drained in one sweep.

The condition it exposed

Both runs still exit 1, for one reason:

Head branch jdeniau-GHSA-xvcm-6775-5m9r now points to f12cc7d..., not 9ff0d0e...
Failed to reconcile pull request 9129 (jdeniau/advisory-improvement-9129)

This is delete_branch's SHA guard, and it is working correctly. Traced:

  • PR 9129 closed unmerged, head jdeniau-GHSA-xvcm-6775-5m9r @ 9ff0d0e
  • One more commit pushed to that branch — f12cc7d, parent 9ff0d0e
  • PR 9226 opened from the same branch @ f12cc7dand it is still open

Deleting that branch would have broken a live pull request. The guard prevented it.

Why this still needs changing

The behaviour is right; the severity is wrong.

The state is permanent for as long as PR 9226 stays open (last touched 26 Aug), so the workflow would be red on every run indefinitely — the same alert fatigue #9332 set out to remove, just from a different cause. We'd have traded a 404 for a stuck branch.

It's also miscategorised. Every other "nothing safe to do here" state already returns 0: pull request still open, fork-headed, deleted. A reused head branch belongs in that bucket.

And it needs no human intervention — it self-heals. Once PR 9226 closes, its own cleanup deletes the head branch; PR 9129's head then reads as already absent → returns 0 → the leftover staging branch is collected on the next sweep.

The change

  • delete_branch returns 3 for this case specifically, with a ::warning:: instead of ::error::. A genuine deletion failure still returns 1, still fails the run, and still prevents the staging branch from being deleted after it.
  • process_pr treats 3 as a recorded skip and returns 0.
  • Skipped branches and the sweep counters are written to the job summary, so they stay visible without being buried in a 1,600-line log and without holding the run red.

Nothing that was previously deleted is deleted any differently. Only the reporting changes.

Rendered summary:

Staging branch cleanup

Metric Count
Staging branches inspected 1467
Pull requests reconciled 1
Left in place (head branch in use) 1
Pull request failures 0
Triage batches failed 0
Head branch reused by a newer pull request (1)

Deleting these branches would break the newer pull request that now uses them, so they were left alone. They are cleaned up automatically once that pull request is closed. No action is needed.

Validation

actionlint, bash -n, shellcheck clean. 19/19 assertions pass, including:

  • The real stuck caseprocess_pr 9129 against the live repo now returns 0, emits a warning not an error, records the skip, and leaves both branches in place (verified they still exist afterwards)
  • A genuine head-deletion failure still returns 1 and still prevents the staging branch from being deleted
  • Return code 3 does not delete the staging branch either
  • A staging-branch deletion failure still fails
  • Deletion ordering unchanged (head first, staging last)
  • Existing skips unchanged: open PR, retargeted base, missing PR on dispatch (1) vs sweep (0)
  • Summary omits an empty section on per-PR runs; unset GITHUB_STEP_SUMMARY is safe under set -u
  • All three trigger paths exit correctly with the new EXIT trap in place

Note for reviewers

jdeniau-GHSA-xvcm-6775-5m9r needs no action — PR 9226 is legitimately using it. It resolves itself when that PR closes.

Unchanged from #9332: ~1,300 of the remaining 1,467 branches are fork-headed and still out of scope, and the 63 orphaned staging branches still need a separate one-off decision.

…quest

Now that the sweep runs to completion it surfaces a condition that was
previously unreachable. When a contributor closes a pull request and then
reuses its head branch for a new one, the head branch no longer points at
the SHA the closed pull request recorded. `delete_branch` correctly refuses
to delete it -- deleting it would break the newer pull request -- but it
reported that refusal as a failure, so the whole run went red.

That state is permanent for as long as the newer pull request stays open, so
the workflow would have been red on every run indefinitely. That is the same
alert fatigue the previous fix set out to remove, just from a different
cause. `jdeniau-GHSA-xvcm-6775-5m9r` is a live example: pull request 9129 was
closed, one more commit was pushed to its head branch, and pull request 9226
was opened from it and is still open.

Reusing a branch this way is a normal contributor pattern here, not an
operational fault, and it needs no human intervention: once the newer pull
request is closed its own cleanup removes the branch, the older pull
request's head then reads as already absent, and the leftover staging branch
is collected on the next sweep. Treat it the way the other "nothing safe to
do here" states are already treated -- still open, fork-headed, deleted --
and skip it instead of failing.

`delete_branch` now returns 3 for this case specifically, so a genuine
deletion failure is still a failure and still stops the staging branch from
being deleted after it. Nothing that was previously deleted is deleted any
differently; only the reporting changes.

Record the skipped branches and write them to the job summary along with the
sweep counters, so they stay visible without being buried in a 1,600-line log
and without holding the run red.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: daca0885-6779-4adb-bb02-ff6920d73ab3
Copilot AI balanced review requested due to automatic review settings September 3, 2026 16:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

SHA mismatches are treated as proof of branch reuse without verifying an open pull request exists.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 1 High severity

New issues introduced by this change (1)
Severity Finding
High severity .github/​workflows/​delete_staging_and_head_branches_writer.yaml — A SHA mismatch does not prove that a newer pull request is using the branch: it also occurs if…
What changed in this PR

Updates branch cleanup to treat reused head branches as skips and report sweep results in the job summary.

Changes:

  • Adds skip tracking for SHA mismatches.
  • Adds reconciliation metrics and an exit-time summary.
File Description
.github/​workflows/​delete_staging_and_head_branches_writer.yaml Handles branch skips and generates cleanup summaries.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/delete_staging_and_head_branches_writer.yaml Outdated
A SHA mismatch was being reported as a self-healing skip on the assumption
that a newer pull request had reused the branch. That assumption does not
hold: a push after closure, or a ref deleted and recreated, produces the same
mismatch. Those branches would have been reported as "no action needed" on
every run while nothing ever collected them, so they would leak permanently.

Confirm over the API that an open pull request currently has the branch as its
head ref before skipping, and name that pull request in the warning and the job
summary. An unexplained mismatch is now a failure that asks for investigation,
and a failed lookup fails closed rather than being read as "nothing is using
this branch".

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: daca0885-6779-4adb-bb02-ff6920d73ab3
@aparnajyothi-y
aparnajyothi-y requested a balanced review from Copilot September 3, 2026 16:34
@taladrane
taladrane merged commit 7d66f12 into main Sep 3, 2026
4 of 5 checks passed
@taladrane
taladrane deleted the handle-reused-head-branches branch September 3, 2026 16:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Skip-record write failures can be masked, producing a successful run with an incomplete summary.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 1 Medium severity

New issues introduced by this change (1)
Severity Finding
Medium severity .github/​workflows/​delete_staging_and_head_branches_writer.yaml — This write is unguarded even though process_pr runs under if ! during a sweep, which disables…
Issues resolved since last review (1)
Severity Finding
High severity .github/​workflows/​delete_staging_and_head_branches_writer.yaml — A SHA mismatch does not prove that a newer pull request is using the branch: it also occurs if… View resolved comment
Suppressed comments (1)

.github/workflows/delete_staging_and_head_branches_writer.yaml:365

  • As in the same-ref path, this unguarded append can fail silently because the sweep invokes process_pr from an if ! condition and the next command returns success. Explicitly propagate the recording failure so the run cannot claim success without reporting the skipped branch.
              record_skip "${pr_number}" "${head_ref}" "${BLOCKING_PR}"

delete_branch "${base_ref}" "${head_sha}" || return 1
delete_branch "${base_ref}" "${head_sha}" || delete_status=$?
if (( delete_status == 3 )); then
record_skip "${pr_number}" "${base_ref}" "${BLOCKING_PR}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants